fix: improve UI responsiveness with large remote hosts files#242
Merged
fix: improve UI responsiveness with large remote hosts files#242
Conversation
Large remote hosts files (e.g. StevenBlack/hosts ~1MB, ~77K domains) caused the app to become unresponsive due to synchronous syntax highlighting processing the entire document on the main thread. - Refactor HostsTextView to highlight large documents (>50KB) in async chunks of ~100KB, yielding to the run loop between chunks. A generation counter cancels stale passes on file switch or user edit. - Replace eager .draggable(hosts.contents()) with lazy .onDrag using NSItemProvider so sidebar rendering no longer reads file contents. - Dispatch all HostsDownloader delegate callbacks to the main thread to fix data races from NSURLSession background queue callbacks. - Add O(1) NSString.length check before O(n) string comparison in HostsTextViewRepresentable.updateNSView.
Add replaceContentWith: method that bypasses the expensive synchronous textStorageDidProcessEditing: callback during bulk text replacement. This avoids the O(n) lineRangeForRange: computation that blocked the main thread on every file switch. Highlighting is instead triggered manually after replacement — async for large files, batched sync for small files.
Add tests proving text view layer is fast: - Small file switching: ~5ms per switch - replaceContentWith: no regression vs direct assignment - No notification cascade during selection changes - No HostsNodeNeedsUpdate posted during selection
The updateNSView guard was comparing the full text content (O(n)) on every @published property change, not just selection changes. For a 16K-line file, each comparison took ~22ms, causing visible lag when multiple re-renders occurred per click. Fix: decouple HostsTextViewRepresentable from the monolithic store and use a two-tier guard: - O(1) pointer check for selection changes (always replace) - Token-based check for external content updates (compare only when rowRefreshToken changes) - Skip entirely when neither selection nor token changed Also adds integration tests for the full HostsDataStore → updateNSView pipeline, objectWillChange publication counting, and pointer-based guard verification.
…cking When switching to a large hosts file (>50K chars), replaceContentWith: called highlightAsyncFrom:0 synchronously, blocking the main thread for ~20ms on a 1.38MB file. Dispatch the first chunk via dispatch_async like subsequent chunks, reducing switch time to ~1.5ms.
Three targeted fixes for UI lockup when switching between local files while a large remote file (e.g. StevenBlack ~1MB) is configured: - Coalesce rowRefreshToken updates in HostsDataStore so 9-12 rapid notifications from a download lifecycle produce 1 SwiftUI re-render instead of 9-12 - Remove duplicate HostsFileSavedNotification in RemoteHostsManager (hostsController saveHosts: already posts it) - Make dscacheutil -flushcache non-blocking using terminationHandler instead of [task waitUntilExit] (~9ms saved per call)
SwiftUI's .onDrag intercepts mouseDown events, which is the same event List uses for row selection. This caused clicks on the icon and text area of sidebar rows to not register as selection — only clicks on empty space worked, making the UI feel unresponsive.
Restore `import UniformTypeIdentifiers` in SidebarView — the drop delegate still uses UTType.fileURL and UTType.url. Relying on SwiftUI's transitive re-export is fragile across SDK versions. Add logDebug for non-zero dscacheutil exit status so failures are visible in debug logs instead of silently ignored.
The 50ms per-render assertion was too tight for CI Intel x86_64 runners where SwiftUI layout in a full NavigationSplitView has high variance under load. Increase to 200ms to match the concurrent download test threshold while still catching real regressions.
The macOS 26 Intel runner timed out at ~55s because 30K-line content triggered expensive async highlighting during RunLoop drains. This test measures re-render cost per notification, not large file highlighting (other tests cover that). Reduce to 5K lines and shorten the initial render wait.
The macOS 26 Intel CI runner consistently times out (~54s) with the full test suite generating multiple 30K-line strings. Reduce all tests to 5K lines (175KB), which still exceeds the 50KB async highlight threshold and exercises the same code paths while being 6x lighter.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Large remote hosts files (~1MB, e.g. StevenBlack/hosts) caused two categories of UI issues: freezes during loading/syntax highlighting, and unresponsive sidebar clicks.
Sidebar click responsiveness
.onDragon sidebar rows interceptedmouseDownevents, preventing SwiftUI'sListfrom registering clicks on the icon and text — only clicks on empty space selected a row.onDragfrom sidebar rows, restoring normal click-to-select behaviorLarge file rendering
HostsTextViewto highlight large documents (>50KB) asynchronously in ~100KB chunks, yielding to the run loop between each chunkHostsTextViewRepresentable.updateNSViewto skip redundant content replacementNSString.lengthcomparison before O(n) string equality checkDownload lifecycle main thread blocking
rowRefreshTokenupdates so 9-12 rapid notifications produce one SwiftUI re-render instead of manyHostsFileSavedNotificationpost inRemoteHostsManager.hostsDownloaded:that causedCombinedHostsControllerto regenerate and save twice per downloaddscacheutil -flushcachenon-blocking usingterminationHandlerinstead ofwaitUntilExitOther fixes
HostsDownloaderdelegate callbacks to the main thread to fix data races fromNSURLSessionbackground queueTest plan
https://raw.githubusercontent.com/StevenBlack/hosts/master/hostsas a remote hosts file